package evotingclient.model;

import evotingclient.data.Candidate;
import javax.net.ssl.KeyManagerFactory;
import evotingclient.data.Card;
import evotingclient.data.InitialData;
import evotingclient.data.NumberCard;
import evotingclient.data.Vote;
import evotingclient.data.VotingData;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.math.BigInteger;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.LinkedList;
import java.util.List;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import javax.security.cert.CertificateException;
import javax.xml.bind.JAXBException;
import javax.xml.stream.XMLStreamException;
import org.apache.mina.core.service.IoConnector;
import org.apache.mina.transport.socket.nio.NioSocketConnector;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.output.XMLOutputter;
import javax.xml.bind.DatatypeConverter;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import org.apache.mina.core.buffer.IoBuffer;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;

/**
 *
 * @author Pawel
 */
public class SSLConnector {

    private static final int REMOTE_PORT = 9999;
    Model model;

    /**
     * 
     * @param model
     */
    public SSLConnector(Model model) {
        this.model = model;
    }

    private SSLSocketFactory getFactory(File pKeyFile, String pKeyPassword) throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, KeyManagementException, java.security.cert.CertificateException, Exception {
        return getFactory(pKeyFile, pKeyPassword, false);
    }

    private SSLSocketFactory getFactory(File pKeyFile, String pKeyPassword, boolean clientAuth) throws NoSuchAlgorithmException, KeyStoreException, IOException, CertificateException, KeyManagementException, java.security.cert.CertificateException, Exception {
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance("SunX509");
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("SunX509");
        KeyStore trustStore = KeyStore.getInstance("JKS");
        KeyStore keyStore = KeyStore.getInstance("PKCS12");

        SSLContext ctx;
        ctx = SSLContext.getInstance("TLS");

        trustStore.load(new FileInputStream(pKeyFile), pKeyPassword.toCharArray());
        if (clientAuth) {
            File ksf = new File(pKeyFile.getPath().replace(".jks", ".p12"));
            System.out.println(ksf.getPath());
            keyStore.load(new FileInputStream(ksf), pKeyPassword.toCharArray());
            keyManagerFactory.init(keyStore, pKeyPassword.toCharArray());
        }
        trustManagerFactory.init(trustStore);

        if (clientAuth) {
            ctx.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
        } else {
            ctx.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
        }
        return ctx.getSocketFactory();
    }

    /**
     * 
     * @param nick
     * @param password
     * @throws Exception
     */
    public void register(String nick, String password) throws Exception {
        IoConnector connector = new NioSocketConnector();
        connector.getSessionConfig().setReadBufferSize(8192);
        try {
            /*
             * Inicjowanie SSL
             */
            File pKeyFile = new File(model.getPath(CertificateManager.keystore));
            // temporary, just to check
            //SSLSocketFactory sslsocketfactory = (SSLSocketFactory) getFactory(new File("clientkeystore.jks"), "bskbsk");
            SSLSocketFactory sslsocketfactory = (SSLSocketFactory) getFactory(pKeyFile, password);
            SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket("localhost", REMOTE_PORT);
            sslsocket.setUseClientMode(true);
            InputStream inputstream = sslsocket.getInputStream();
            InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
            BufferedReader bufferedreader = new BufferedReader(inputstreamreader);

            OutputStream outputstream = sslsocket.getOutputStream();
            OutputStreamWriter outputstreamwriter = new OutputStreamWriter(outputstream);
            BufferedWriter bufferedwriter = new BufferedWriter(outputstreamwriter);

            String string = null;

            sslsocket.startHandshake();



            /*
             * Sending registration message
             */
            System.out.println("Będę wysyłał wiadomość");
            Element ss = new Element("session");
            ss.setAttribute("name", "registration");
            Element msg = new Element("msg");
            Document doc = new Document(ss);

            PersonalData pd = model.getPersonalData();
            msg.addContent(MyXmlParser.parseToXmlSimpleObject(pd));

            // Certificate cert = model.getCertificate();
            byte[] pkcs10 = model.getSingRequst();
            Element singrec = new Element("PKCS10");
            singrec.addContent(bytesToString(pkcs10));

            msg.addContent(singrec);
            ss.addContent(msg);


            XMLOutputter out = new XMLOutputter();
            out.setFormat(Format.getPrettyFormat());

            /*String str = out.outputString(doc);
            IoBuffer buf = IoBuffer.allocate(str.length()+5);
            buf.putString(str, null);*/

            bufferedwriter.write(out.outputString(doc));
            bufferedwriter.flush();
            System.out.println("Wysłałem");


            /*
             * Odbieranie podpisanego certyfikatu
             * i listy kandydatów
             */
            //string = bufferedreader.readLine();
            BufferedInputStream bis = new BufferedInputStream(sslsocket.getInputStream());
            byte[] recvData = new byte[18192];
            bis.read(recvData);
            string = (new String(recvData)).trim();
            System.out.println("3: .... " + string);
            SAXBuilder builder = new SAXBuilder();
            try {
                Document document = builder.build(new StringReader(string));
                Element root = document.getRootElement();
                if (root.getName().equals("session")) {
                    String sessionName = root.getAttribute("name").getValue();
                    if (sessionName.equals("registration")) {
                        msg = root.getChild("msg");
                        String result = msg.getChild("result").getValue();
                        if (!result.equals("OK")) {
                            throw new Exception("Nie udało się podpisać certyfikatu\nPowód: " + result);
                        } else {

                            // Składamy certyfikat do kupy
                            Element cert = msg.getChild("signedCertificate").getChild("Certificate");
                            List<Element> partList = cert.getChildren("part");
                            LinkedList<String> parts = new LinkedList<String>();
                            for (Element p : partList) {
                                int i = Integer.parseInt(p.getAttributeValue("sequence"));
                                parts.add(i - 1, p.getValue());
                            }
                            StringBuilder sb = new StringBuilder();
                            for (int i = 0; i < parts.size(); i++) {
                                sb.append(parts.get(i)); // z pewnością strasznie nieefektywne, ale trudno
                            }
                            String signed_cert = sb.toString();
                            FileOutputStream fw = new FileOutputStream(model.getPath(nick + "_signed.cer"));
                            fw.write(stringToBytes(signed_cert));
                            fw.close();
                            BufferedWriter bw = new BufferedWriter(new FileWriter(model.getPath("candidates.txt")));


                        }
                    }
                }
            } catch (JDOMException e) {
                System.out.println("Malformed XML");
                System.out.println(e.getMessage());
            }

        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    public void voitingProcess(String nick, String password) throws Exception {
        IoConnector connector = new NioSocketConnector();
        connector.getSessionConfig().setReadBufferSize(8192);
        try {
            /*  
             * Inicjowanie SSL
             */
            File pKeyFile = new File(model.getPath(CertificateManager.keystore));
            SSLSocketFactory sslsocketfactory = (SSLSocketFactory) getFactory(pKeyFile, password);
            System.out.println(pKeyFile.getPath());
            SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket("localhost", REMOTE_PORT);
            sslsocket.setUseClientMode(true);
            InputStream inputstream = sslsocket.getInputStream();
            InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
            BufferedReader bufferedreader = new BufferedReader(inputstreamreader);

            OutputStream outputstream = sslsocket.getOutputStream();
            OutputStreamWriter outputstreamwriter = new OutputStreamWriter(outputstream);
            BufferedWriter bufferedwriter = new BufferedWriter(outputstreamwriter);

            String string = null;

            System.out.println("Starting SSL client");
            System.out.println(sslsocket.getSSLParameters());
            sslsocket.startHandshake();

            /*
             * Sending registration message
             */
            System.out.println("Będę wysyłał wiadomość");
            Element ss = new Element("session");
            ss.setAttribute("name", "registration");
            Element msg = new Element("msg");
            Document doc = new Document(ss);

            PersonalData pd = model.getPersonalData();
            msg.addContent(MyXmlParser.parseToXmlSimpleObject(pd));

            // Certificate cert = model.getCertificate();
            byte[] pkcs10 = model.getSingRequst();
            Element singrec = new Element("PKCS10");
            singrec.addContent(bytesToString(pkcs10));

            msg.addContent(singrec);
            ss.addContent(msg);


            XMLOutputter out = new XMLOutputter();
            out.setFormat(Format.getPrettyFormat());

            /*String str = out.outputString(doc);
            IoBuffer buf = IoBuffer.allocate(str.length()+5);
            buf.putString(str, null);*/

            bufferedwriter.write(out.outputString(doc));
            bufferedwriter.flush();
            System.out.println("Wysłałem");


            /*
             * Odbieranie podpisanego certyfikatu
             * i listy kandydatów
             */
            //string = bufferedreader.readLine();
            BufferedInputStream bis = new BufferedInputStream(sslsocket.getInputStream());
            byte[] recvData = new byte[18192];
            bis.read(recvData);
            string = (new String(recvData)).trim();


        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }

    private String bytesToString(byte[] bytes) {
        return DatatypeConverter.printBase64Binary(bytes);
    }

    private byte[] stringToBytes(String string) {
        return DatatypeConverter.parseBase64Binary(string);
    }

    public SSLSocket initialSSL(String password) {
        IoConnector connector = new NioSocketConnector();
        connector.getSessionConfig().setReadBufferSize(18192);
        try {
            /*
             * Inicjowanie SSL
             */
            File pKeyFile = new File(model.getPath(CertificateManager.keystore));
            // temporary, just to check
            //SSLSocketFactory sslsocketfactory = (SSLSocketFactory) getFactory(new File("clientkeystore.jks"), "bskbsk");
            SSLSocketFactory sslsocketfactory = (SSLSocketFactory) getFactory(pKeyFile, password, true);
            System.out.println(pKeyFile.getPath());
            SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket("localhost", REMOTE_PORT);
            sslsocket.setUseClientMode(true);
            sslsocket.startHandshake();
            return sslsocket;
        } catch (Exception exception) {
            exception.printStackTrace();
        }
        return null;
    }

    public InitialData getInitialData(SSLSocket sslsocket) {
        try {
            String string = null;



            /*
             * Sending registration message
             */
            IoBuffer buf = IoBuffer.allocate(18192, true);
            JAXBContext context;
            context = JAXBContext.newInstance(InitialData.class);


            OutputStream outputstream = sslsocket.getOutputStream();
            OutputStreamWriter outputstreamwriter = new OutputStreamWriter(outputstream);
            outputstream.flush();
            BufferedWriter bufferedwriter = new BufferedWriter(outputstreamwriter, 18192);


            /*
             * Send request for initial data
             */
            Element ss = new Element("session");
            ss.setAttribute("name", "cardgen");
            Element msg = new Element("msg");
            Document doc = new Document(ss);
            msg.addContent("init");
            ss.addContent(msg);

            XMLOutputter out = new XMLOutputter();
            out.setFormat(Format.getPrettyFormat());

            bufferedwriter.write(out.outputString(doc));
            bufferedwriter.flush();


            BufferedInputStream bis = new BufferedInputStream(sslsocket.getInputStream());
            byte[] recvData = new byte[18192];
            bis.read(recvData);
            string = (new String(recvData)).trim();
            System.out.println("String klienta: " + string);
            InitialData idata = parseInitialData(string);
            return idata;
        } catch (Exception exception) {
            exception.printStackTrace();
        }
        return null;
    }

    private InitialData parseInitialData(String string) throws XMLStreamException, JAXBException, IOException {
        SAXBuilder builder = new SAXBuilder();
        try {
            Document document = builder.build(new StringReader(string));
            Element root = document.getRootElement();
            Element xmlInitData = root.getChild("msg").getChild("initialData");
            InitialData idata = new InitialData();
            idata.setE(new BigInteger(xmlInitData.getChild("e").getValue()));
            idata.setN(new BigInteger(xmlInitData.getChild("n").getValue()));
            idata.setNumberOfCard(Integer.parseInt(xmlInitData.getChildText("numberOfCard")));
            List<Element> candidates = root.getChild("msg").getChild("Candidates").getChildren("Candidate");
            List<Candidate> cds = new LinkedList<Candidate>();
            for (Element c : candidates) {
                cds.add(new Candidate(Integer.parseInt(c.getAttribute("id").getValue()),
                        c.getAttribute("name").getValue()));
            }
            idata.setCandidates(cds);
            return idata;
        } catch (Exception e) {
        }


        return null;
    }

    private NumberCard parseNumberCard(String string) throws XMLStreamException, JAXBException {
        SAXBuilder builder = new SAXBuilder();
        try {
            Document document = builder.build(new StringReader(string));
            Element root = document.getRootElement();
            return new NumberCard(Integer.parseInt(root.getChild("msg").getChild("cardNumber").getValue()));
        } catch (JDOMException e) {
            System.out.println("Malformed XML");
            System.out.println(e.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    Integer sendBlind(SSLSocket sslsocket, VotingData votingData) {
        try {
            String string = null;
            

            Element ss = new Element("session");
            ss.setAttribute("name", "cardgen");
            Element msg = new Element("msg");
            Document doc = new Document(ss);
            ss.addContent(msg);

            Element xmlVotingData = new Element("votingData");
            msg.addContent(xmlVotingData);
            for (Card c : votingData.getCards()) {
                Element card = new Element("Card");
                card.setAttribute("number", c.getNumber().toString());
                Element xmlVotes = new Element("votes");
                card.addContent(xmlVotes);
                for (Vote v : c.getVotes()) {
                    Element blindVote = new Element("blindVote");
                    blindVote.addContent(v.getBlindVote().toString());
                    xmlVotes.addContent(blindVote);
                }
                xmlVotingData.addContent(card);
            }
            xmlVotingData.addContent(new Element("name").addContent(votingData.getName()));
            xmlVotingData.addContent(new Element("sId").addContent(votingData.getsId().toString()));

            XMLOutputter out = new XMLOutputter();
            out.setFormat(Format.getPrettyFormat());
            String str = out.outputString(doc);
            System.out.println(str);
            
            int length = str.length()*4 + 1000;
            sslsocket.setSendBufferSize(length);
            OutputStream outputstream = sslsocket.getOutputStream();
            OutputStreamWriter outputstreamwriter = new OutputStreamWriter(outputstream);
            BufferedWriter bufferedwriter = new BufferedWriter(outputstreamwriter, length);
            bufferedwriter.flush();
            System.out.println(sslsocket.getSendBufferSize());
            System.out.println(length);

            bufferedwriter.newLine(); // nie wiedzieć czemu bez tego nie działa
            bufferedwriter.write(str);
            bufferedwriter.flush();


            BufferedInputStream bis = new BufferedInputStream(sslsocket.getInputStream());
            byte[] recvData = new byte[18192];
            bis.read(recvData);
            System.out.println(recvData);
            string = (new String(recvData)).trim();
            
            // cuś się psuje, więc trzeba naprawiać bruteforcem
            if (string.length() < 10) {
                bis.read(recvData);
                System.out.println(recvData);
                string += new String(recvData).trim();
            }
            System.out.println("Odbieram numer: " + string);
            NumberCard number = parseNumberCard(string);

            return number.getNumber();

        } catch (Exception exception) {
            exception.printStackTrace();
        }
        return null;

    }

    Card sendVotingData(SSLSocket sslsocket, VotingData votingData) {
        try {
            String string = null;
            
            /*
             * Wysyłanie numerów do de-blindu
             */
            Element ss = new Element("session");
            ss.setAttribute("name", "cardgen");
            Element msg = new Element("msg");
            Document doc = new Document(ss);
            ss.addContent(msg);
            msg.addContent("unblinding data");
            Element ubd = new Element("unblindData");
            ss.addContent(ubd);
            for(Card c : votingData.getCards()) {
                Element crd = new Element("card");
                crd.setAttribute("number", c.getNumber().toString());
                crd.setAttribute("r", c.getR().toString());
                ubd.addContent(crd);
            }

            OutputStream outputstream = sslsocket.getOutputStream();
            OutputStreamWriter outputstreamwriter = new OutputStreamWriter(outputstream);
            
            XMLOutputter out = new XMLOutputter();
            out.setFormat(Format.getPrettyFormat());
            String str = out.outputString(doc);
            //System.out.println(str);
            

            BufferedWriter bufferedwriter = new BufferedWriter(outputstreamwriter, str.length()*16 + 1000);
            bufferedwriter.newLine(); // nie wiedzieć czemu bez tego nie działa
            bufferedwriter.write(str);
            bufferedwriter.flush();

            BufferedInputStream bis = new BufferedInputStream(sslsocket.getInputStream());
            byte[] recvData = new byte[18192];
            bis.read(recvData);
            string = (new String(recvData)).trim();
            // cuś się psuje, więc trzeba naprawiać bruteforcem
            if (string.length() < 10) {
                bis.read(recvData);
                System.out.println(recvData);
                string += new String(recvData).trim();
            }
            Card card = parseCard(string);
            return card;
        } catch (Exception exception) {
            exception.printStackTrace();
        }
        return null;

    }

    private Card parseCard(String string) throws XMLStreamException, JAXBException {
        SAXBuilder builder = new SAXBuilder();
        System.out.println(string);
        try {
            Document document = builder.build(new StringReader(string));
            Element root = document.getRootElement();
            Element signedCard = root.getChild("msg").getChild("signedCard");
            if(signedCard != null) {
                Card card = new Card();
                card.setNumber(signedCard.getAttribute("number").getIntValue());
                for(Element bsv : (List<Element>)signedCard.getChildren("blindSignedVote")) {
                    Vote nv = new Vote();
                    nv.setBlindSignVote(new BigInteger(bsv.getValue()));
                    card.getVotes().add(nv);
                }
                return card;
            }
        } catch (JDOMException e) {
            System.out.println("Malformed XML");
            System.out.println(e.getMessage());
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}